/*
 * GIntCupImpl.java
 * Created on 23 nov. 2008
 *
 * Glacéo Internet Platform
 * http://sourceforge.net/projects/chm/
 *
 * Copyright (c) 2005-2009, Jens and Jan Janke (VirtualHockey Project)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hockeo.vhbase.data.impl.contest;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Required;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.hockeo.vhbase.data.dao.IGDao;
import com.hockeo.vhbase.data.dao.club.GClub;
import com.hockeo.vhbase.data.dao.club.GClubAbstractIntCupHistoryRecord;
import com.hockeo.vhbase.data.dao.club.GClubCLHistoryRecord;
import com.hockeo.vhbase.data.dao.contest.GContest;
import com.hockeo.vhbase.data.dao.contest.GContestElement;
import com.hockeo.vhbase.data.dao.contest.GContestHistoryRecord;
import com.hockeo.vhbase.data.dao.contest.GContestRound;
import com.hockeo.vhbase.data.dao.contest.GKO3ContestElement;
import com.hockeo.vhbase.data.dao.contest.GRoundRobinContestElement;
import com.hockeo.vhbase.data.dao.match.GKOMatch;
import com.hockeo.vhbase.data.dao.season.GMatchday;
import com.hockeo.vhbase.data.dao.season.GMoveday;
import com.hockeo.vhbase.data.dao.season.GSeason;
import com.hockeo.vhbase.data.dao.standings.GGoalsAccount;
import com.hockeo.vhbase.data.dao.standings.GStandings;
import com.hockeo.vhbase.data.helper.GContestRoundFormat;
import com.hockeo.vhbase.data.helper.GContestRoundType;
import com.hockeo.vhbase.data.helper.GStandingsComparator;
import com.hockeo.vhbase.data.helper.GStandingsRankType;
import com.hockeo.vhbase.data.helper.GStandingsSortCriteria;
import com.hockeo.vhbase.data.helper.GStandingsType;
import com.hockeo.vhbase.data.impl.IGContestImpl;
import com.hockeo.vhbase.data.impl.goals.IGGoalsDistributor;
import com.hockeo.vhbase.error.GDataException;
import com.hockeo.vhbase.error.GException;
import com.hockeo.vhbase.seasoncycle.services.GRoundRobinMatchCreator;

/**
 * Abstract base class for international cup implementations (mixed type multi round
 * contests).
 *
 * @version $Id$
 * @author jjanke
 */
@Transactional( propagation = Propagation.MANDATORY, rollbackFor = { GException.class } )
public abstract class GIntCupImpl implements IGContestImpl
{
  protected IGDao                   d_dao;
  protected GRoundRobinMatchCreator d_rrMatchGenerator;
  protected IGGoalsDistributor      d_goalsDistributor;

  @Required
  public void setDao( IGDao dao )
  {
    d_dao = dao;
  }

  @Required
  public void setGoalsDistributor( IGGoalsDistributor goalsDistributor )
  {
    d_goalsDistributor = goalsDistributor;
  }

  public void undoCreateStructure( GSeason season, GContest contest ) throws GDataException
  {
    // Remove club/cup history elements
    //
    d_dao.deleteClubHistoryRecords( season, contest );

    // Remove contest rounds (elements will be removed automatically because of the
    // cascaded removal)
    //
    if ( contest.getContestRounds() != null )
      for ( GContestRound round : contest.getContestRounds() )
        d_dao.delete( round );
  }

  public void deleteStructure( GSeason season, GContest contest ) throws GDataException
  {
    // Remove contest rounds (elements will be removed automatically because of the
    // cascaded removal)
    //
    if ( contest.getContestRounds() != null )
      for ( GContestRound round : contest.getContestRounds() )
        d_dao.delete( round );
  }

  public void createMatchdays( GContest contest ) throws GDataException
  {
    // We have to make sure that the matchdays are correctly numbered from 1 to N
    //
    int nMatchdayCnt = 0;

    for ( GContestRound round : contest.getContestRounds() )
    {
      int nMaxMatchdaysPerRound = 0;

      for ( GContestElement element : round.getContestElements() )
      {
        int i = 1;
        int nNumMatchdays = element.getNumMatchdays();
        for ( ; i <= nNumMatchdays; i++ )
        {
          GMatchday matchday = new GMatchday( element, nMatchdayCnt + i, i == 1, i == nNumMatchdays );

          d_dao.makePersistent( matchday );
        }

        if ( i - 1 > nMaxMatchdaysPerRound )
          nMaxMatchdaysPerRound = i - 1;
      }

      nMatchdayCnt += nMaxMatchdaysPerRound;
    }
  }

  public void createNextContestRound( GContest contest, GSeason season, GMoveday movedayCurrent ) throws GDataException
  {
    GContestRound round = d_dao.findFirstNonPlayedContestRound( contest );

    // If all rounds are played, nothing needs to be created anymore
    //
    if ( round == null )
      return;

    // We have to create KO rounds for the qualification and the final and round robin
    // rounds for the rest
    //
    if ( round.getContestElements().get( 0 ).instanceOf( GKO3ContestElement.class ) )
    {
      // KO rounds must not have more than one contest element
      //
      GKO3ContestElement element = (GKO3ContestElement) round.getContestElements().get( 0 );

      createKO3GoalAccounts( season, element );
      createKO3Matches( season, element );
    }
    else if ( round.getContestElements().get( 0 ).instanceOf( GRoundRobinContestElement.class ) )
    {
      createRRGoalAccounts( season, round );
      createRRMatches( round );
    }
    else
      throw new IllegalArgumentException( "The contest round " + round.getId() + " has an unsupported element type "
          + round.getContestElements().get( 0 ).getGEntityClass().getName() );
  }

  /**
   * Creates the goal accounts for a KO3 contest element. Please override this method if a
   * specific implementation is required. By default the standard
   * {@link GContestImplUtils} implementation is used.
   *
   * @param season the current season
   * @param element the KO3 contest element for which to create goal accounts
   * @throws GDataException
   */
  protected void createKO3GoalAccounts( GSeason season, GKO3ContestElement element ) throws GDataException
  {
    d_goalsDistributor.initialize( element );
    GContestImplUtils.createGoalsAccounts( d_dao, d_goalsDistributor, season, element, true );
  }

  /**
   * Creates the matches for a KO3 round. Please override this method if a specific
   * implementation is required. By default the standard {@link GContestImplUtils}
   * implementation is used.
   *
   * @param season the current season
   * @param element the KO3 contest element for which to create matches
   * @throws GDataException
   */
  protected void createKO3Matches( GSeason season, GKO3ContestElement element ) throws GDataException
  {
    GContestImplUtils.createMatches( d_dao, season, element, false );
  }

  /**
   * Creates the goal accounts for all contest elements in a round robin round. Please
   * override this method if a specific implementation is required. By default the
   * standard {@link GContestImplUtils} implementation is used.
   *
   * @param season the current season
   * @param round the RR contest round for which to create goal accounts
   * @throws GDataException
   */
  protected void createRRGoalAccounts( GSeason season, GContestRound round ) throws GDataException
  {
    d_goalsDistributor.initialize( round.getContestElements().get( 0 ) );
    GContestImplUtils.createGoalsAccounts( d_dao, d_goalsDistributor, season, round, true );
  }

  /**
   * Creates the matches for all contest elements of the given round robin round. Please
   * override this method if a specific implementation is required. By default the
   * standard {@link GContestImplUtils} implementation is used.
   *
   * @param round the RR contest round for which to create matches
   * @throws GDataException
   */
  protected void createRRMatches( GContestRound round ) throws GDataException
  {
    if ( d_rrMatchGenerator == null )
      d_rrMatchGenerator = new GRoundRobinMatchCreator( d_dao );

    for ( GContestElement ce : round.getContestElements() )
    {
      GRoundRobinContestElement element = d_dao.findEntity( GRoundRobinContestElement.class, ce.getId() );

      GContestImplUtils.createMatches( d_dao, element, d_rrMatchGenerator );
    }
  }

  public void undoCreateNextContestRound( GContest contest, GMoveday movedayLastPrepared ) throws GDataException
  {
    GContestRound round = d_dao.findFirstNonPlayedContestRound( contest );

    if ( round == null )
      return;

    // Differenciate between KO and RR rounds
    //
    if ( round.getContestElements().get( 0 ).instanceOf( GKO3ContestElement.class ) )
    {
      // KO rounds must not have more than one contest element
      //
      d_dao.deleteGoalAccountsMatchesFreeTicketsAndStandings( round.getContestElements().get( 0 ) );
    }
    else if ( round.getContestElements().get( 0 ).instanceOf( GRoundRobinContestElement.class ) )
    {
      // We must only do something if the current moveday is the first one of the contest
      // round
      //
      GMatchday matchdayFirst = d_dao.findFirstMatchday( round.getContestElements().get( 0 ) );

      if ( matchdayFirst.getMoveday().getId() == movedayLastPrepared.getId() )
        for ( GContestElement element : round.getContestElements() )
          d_dao.deleteGoalAccountsMatchesFreeTicketsAndStandings( element );
    }
    else
      throw new IllegalArgumentException( "The contest round " + round.getId() + " has an unsupported element type "
          + round.getContestElements().get( 0 ).getGEntityClass().getName() );
  }

  public void analyseMatches( final GContestElement element, final GMatchday matchday ) throws GDataException
  {
    if ( element.instanceOf( GKO3ContestElement.class ) )
      analyseMatchesKORound( d_dao.findEntity( GKO3ContestElement.class, element.getId() ), matchday );
    else if ( element.instanceOf( GRoundRobinContestElement.class ) )
      analyseMatchesRRRound( d_dao.findEntity( GRoundRobinContestElement.class, element.getId() ), matchday );
    else
      throw new IllegalArgumentException( "The contest element " + element.getId() + " is of an unsupported type "
          + element.getGEntityClass().getName() );
  }

  /**
   * Analyses the matches of a KO round.
   *
   * @param element the contest element for which the analysis should be executed
   * @param matchday the matchday to be analysed
   * @throws GDataException
   */
  private void analyseMatchesKORound( final GKO3ContestElement element, final GMatchday matchday )
      throws GDataException
  {
    if ( matchday.isFirst() )
      element.getContestRound().setStarted( true );

    // The actual analysis is only done when the last matchday of the contest element is
    // passed
    //
    if ( !matchday.isLast() )
    {
      matchday.setAnalysed( true );
      return;
    }

    final GSeason season = d_dao.findCurrentSeason();
    final GContestRound roundNext = element.getContest().getNextRound( element.getContestRound() );
    final Map<GClub, GGoalsAccount> mapGAs = d_dao.findGoalsAccountsAsMap( element );
    final Map<GClub, GClubAbstractIntCupHistoryRecord> mapCHRs = d_dao.findClubHistoryRecordsByContestElement(
        GClubAbstractIntCupHistoryRecord.class, season, element );
    final ArrayList<GClubAbstractIntCupHistoryRecord> listLosers = new ArrayList<GClubAbstractIntCupHistoryRecord>();

    for ( GKOMatch komatch : element.getKOMatches() )
    {
      komatch.play( mapGAs.get( komatch.getClubHome() ), mapGAs.get( komatch.getClubAway() ) );

      // Create/update club history record for home team and possibly assign it to
      // next contest round
      //
      GClubAbstractIntCupHistoryRecord chr = mapCHRs.get( komatch.getClubHome() );
      chr.addRoundResult( element, komatch );
      if ( roundNext != null && komatch.isWinner( komatch.getClubHome() ) )
        chr.assignToRound( roundNext );

      if ( komatch.isLoser( komatch.getClubHome() ) )
        listLosers.add( chr );

      // Create/update club history record for away team and possibly assign it to
      // next contest round
      //
      chr = mapCHRs.get( komatch.getClubAway() );
      chr.addRoundResult( element, komatch );
      if ( roundNext != null && komatch.isWinner( komatch.getClubAway() ) )
        chr.assignToRound( roundNext );

      if ( komatch.isLoser( komatch.getClubAway() ) )
        listLosers.add( chr );
    }

    // Establish more precise ranking of all losers
    //
    if ( !element.getContestRound().isLastRound() )
    {
      Collections.sort( listLosers );
      int nRank = 0;
      int nNum = element.getContestRound().getBestPossibleLoserRank();
      GClubAbstractIntCupHistoryRecord chrPrevLoser = null;
      for ( GClubAbstractIntCupHistoryRecord chrLoser : listLosers )
      {
        if ( chrPrevLoser == null || chrLoser.compareTo( chrPrevLoser ) != 0 )
          nRank = nNum;

        chrLoser.setRank( nRank );
        chrPrevLoser = chrLoser;
        nNum++;
      }
    }

    // Handle losers
    //
    handleKORoundLosers( season, element, listLosers );
  }

  /**
   * Allows the implementation to do special handling of KO round losers. Especially
   * useful if e.g. CL qualification losers need to be attached to the WL.
   *
   * @param season the current season
   * @param element the last played KO element
   * @param listLosers the list of losers
   */
  protected void handleKORoundLosers( final GSeason season, final GKO3ContestElement element,
      final ArrayList<GClubAbstractIntCupHistoryRecord> listLosers )
  {
  // by default, nothing is done
  }

  /**
   * Undoes any operation carried out by the
   * {@link #handleKORoundLosers(GSeason, GKO3ContestElement, ArrayList)} method.
   *
   * @param season the current season
   * @param element the last played KO element
   * @param listLosers the list of losers
   */
  protected void resetHandleKORoundLosers( final GSeason season, final GKO3ContestElement element,
      final ArrayList<GClubAbstractIntCupHistoryRecord> listLosers )
  {
  // by default, nothing is done
  }

  /**
   * Analyses the matches of a round robin matchday.
   *
   * @param element the contest element for which the analysis should be executed
   * @param matchday the matchday to be analysed
   * @throws GDataException
   */
  private void analyseMatchesRRRound( final GRoundRobinContestElement element, final GMatchday matchday )
      throws GDataException
  {
    GContestImplUtils.analyseRRMatchday( d_dao, element, matchday );
  }

  /**
   * If the round contains RR elements, the cross-element ranking is established.
   */
  public void postprocessMultiElementRound( final GContestRound round, final Integer nMatchday ) throws GDataException
  {
    int nTeamsPerElement = round.getContestElements().get( 0 ).getNumTeams();

    // Determine cross-element ranking
    //
    List<GStandings> listAllStandings = d_dao.findStandings( round, GStandingsType.TOTAL, nMatchday, 1,
        nTeamsPerElement );
    Collections.sort( listAllStandings, new GStandingsComparator( GStandingsSortCriteria.FOR_MULTIPLE_ELEMENTS ) );

    int nCrossRank = 1;
    for ( GStandings s : listAllStandings )
      s.setCrossElementRank( nCrossRank++ );
  }

  public void createEndOfSeasonStatistics( GContestElement element ) throws GDataException
  {
    final GSeason season = d_dao.findCurrentSeason();
    final GContestRound round = element.getContestRound();

    // Only for RR rounds, we have to update the club/CL history records
    //
    if ( round.getContestElements().get( 0 ).instanceOf( GRoundRobinContestElement.class ) )
    {
      GContestRound roundNext = null;
      GContestRound roundAfterNext = null;
      final GRoundRobinContestElement rrElement = d_dao.findEntity( GRoundRobinContestElement.class, element.getId() );
      final boolean fPromoted1And2 = rrElement.getNumPromoted1() > 0 && rrElement.getNumPromoted2() > 0;
      final List<GStandings> listStandings = d_dao.findFinalStandings( rrElement, GStandingsType.TOTAL );
      final Map<GClub, GClubAbstractIntCupHistoryRecord> mapCHRs = d_dao.findClubHistoryRecordsByContestElement(
          GClubAbstractIntCupHistoryRecord.class, season, element );

      if ( !round.isLastRound() )
      {
        roundNext = element.getContest().getNextRound( round );
        assert roundNext != null : "Although round " + round.getId() + " is not the last, no subsequent round exists.";
      }

      if ( !round.isLastRound() && fPromoted1And2 )
      {
        roundAfterNext = element.getContest().getNextRound( roundNext );
        assert roundAfterNext != null : "Although round " + round.getId()
            + " is not the last and has promoted_1 and promoted_2 teams, no round after the next round exists.";
      }

      for ( GStandings s : listStandings )
      {
        GClubAbstractIntCupHistoryRecord chr = mapCHRs.get( s.getClub() );
        chr.addRoundResult( rrElement, s );

        // Only make next round assignments if this is not the last contest round
        //
        if ( round.isLastRound() )
          continue;

        // Check if there are promoted_1 and promoted_2 categories - if yes, assign
        // promoted_1 teams to the round following the next round and promoted_2 teams to
        // the next round. Otherwise, the promoted_1 teams are simply assigned to the next
        // round.
        //
        if ( fPromoted1And2 )
        {
          if ( s.getRankType() == GStandingsRankType.PROMOTED_1 )
            chr.assignToRound( roundAfterNext );
          else if ( s.getRankType() == GStandingsRankType.PROMOTED_2 )
            chr.assignToRound( roundNext );
        }
        else if ( s.getRankType() == GStandingsRankType.PROMOTED_1 )
          chr.assignToRound( roundNext );
      }
    }

    // If this is called for the final round, we have to create a contest history record
    //
    if ( !round.isLastRound() )
      return;

    GContestHistoryRecord chr;
    if ( round.getFormat() == GContestRoundFormat.KNOCK_OUT )
    {
      GKO3ContestElement koElement = d_dao.findEntity( GKO3ContestElement.class, element.getId() );

      // Contest history record
      //
      GKOMatch komatchFinal = koElement.getKOMatches().get( 0 );
      chr = new GContestHistoryRecord( element.getContest().getHistoryType(), season, koElement.getContest(),
          komatchFinal.getWinner(), komatchFinal.getLoser() );
    }
    else
    {
      List<GStandings> listStandings = d_dao.findFinalStandings( round, GStandingsType.TOTAL, 1, 2 );

      chr = new GContestHistoryRecord( element.getContest().getHistoryType(), season, round.getContest(),
          listStandings.get( 0 ).getClub(), listStandings.get( 1 ).getClub() );
    }

    d_dao.makePersistent( chr );
  }

  public void deleteEndOfSeasonStatistics( GContestElement element ) throws GDataException
  {
    final GSeason season = d_dao.findCurrentSeason();
    final GContestRound round = element.getContestRound();

    // Only for RR rounds, we have to reverse club history record updates
    //
    if ( round.getContestElements().get( 0 ).instanceOf( GRoundRobinContestElement.class ) )
    {
      final GRoundRobinContestElement rrElement = d_dao.findEntity( GRoundRobinContestElement.class, element.getId() );
      final List<GStandings> listStandings = d_dao.findFinalStandings( rrElement, GStandingsType.TOTAL );
      final Map<GClub, GClubCLHistoryRecord> mapCHRs = d_dao.findClubHistoryRecordsByContestElement(
          GClubCLHistoryRecord.class, season, element );

      for ( GStandings s : listStandings )
      {
        GClubCLHistoryRecord chr = mapCHRs.get( s.getClub() );

        if ( round.getRoundType() == GContestRoundType.ROUND_MAIN && !chr.hasPlayedQuali() )
          chr.reset();
        else
          chr.removeRoundResult( rrElement, s );

        chr.assignToRound( round );
      }
    }

    // Finally delete contest history record if we are in the last round
    //
    if ( !round.isLastRound() )
      return;

    GContestHistoryRecord chr = d_dao.findContestHistoryRecord( season, element.getContest() );
    if ( chr != null )
      d_dao.delete( chr );
  }

  public void resetContestStats( GMoveday moveday, GContestElement element ) throws GDataException
  {
    // Only required if the element belongs to a KO round
    //
    if ( !element.instanceOf( GKO3ContestElement.class ) )
      return;

    final GSeason season = d_dao.findCurrentSeason();
    final GKO3ContestElement koelement = d_dao.findEntity( GKO3ContestElement.class, element.getId() );
    final Map<GClub, GClubAbstractIntCupHistoryRecord> mapCHRs = d_dao.findClubHistoryRecordsByContestElement(
        GClubAbstractIntCupHistoryRecord.class, season, element );
    final ArrayList<GClubAbstractIntCupHistoryRecord> listLosers = new ArrayList<GClubAbstractIntCupHistoryRecord>();

    // Remove stats from current round from the club/cup history records
    //
    for ( GKOMatch komatch : koelement.getKOMatches() )
    {
      // Home club (reset history and assign it to current round)
      //
      GClubAbstractIntCupHistoryRecord chr = mapCHRs.get( komatch.getClubHome() );

      if ( koelement.getContestRound().getRoundType() == GContestRoundType.QUALIFICATION_1 )
        chr.reset();
      else
        chr.removeRoundResult( koelement, komatch );

      if ( komatch.isLoser( komatch.getClubHome() ) )
        listLosers.add( chr );

      chr.assignToRound( element.getContestRound() );

      // Away club (reset history and assign it to current round)
      //
      chr = mapCHRs.get( komatch.getClubAway() );

      if ( koelement.getContestRound().getRoundType() == GContestRoundType.QUALIFICATION_1 )
        chr.reset();
      else
        chr.removeRoundResult( koelement, komatch );

      if ( komatch.isLoser( komatch.getClubAway() ) )
        listLosers.add( chr );

      chr.assignToRound( element.getContestRound() );
    }

    // Handle losers
    //
    resetHandleKORoundLosers( season, koelement, listLosers );
  }

  public void resetGoalAccounts( GMoveday moveday, GContestElement element ) throws GDataException
  {
    if ( element.instanceOf( GKO3ContestElement.class ) )
      GContestImplUtils.resetGoalAccounts( d_dao, moveday, d_dao.findEntity( GKO3ContestElement.class, element.getId() ) );
    else if ( element.instanceOf( GRoundRobinContestElement.class ) )
      GContestImplUtils.resetGoalAccounts( d_dao, moveday, d_dao.findEntity( GRoundRobinContestElement.class,
          element.getId() ) );
    else
      throw new IllegalArgumentException( "The contest element " + element.getId() + " is of an unsupported type "
          + element.getGEntityClass().getName() );
  }
}
